package world;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;

import world.placement.Position;
import world.square.Square;
import be.kuleuven.cs.som.annotate.Basic;
import exceptions.IllegalDungeonPlacementException;

/**
 * A class representing a composite dungeon. It exists of levels, shafts or other composites.
 * 
 * @param 	<S>
 * 			The type parameter, which is a square.
 * @invar 	All subdungeons in this composite are valid.
 * 		  	| for each subDungeon in subdungeons:
 * 	      	|		canHaveAsSubDungeon(subDungeon)
 * 
 * @author 	Frederic Mes, Wim Marynissen
 * @version 1.0
 */
public class Composite<S extends Square> extends Dungeon<S>{
	
	/**
	 * Creates a new composed dungeon. It exists of levels, shafts or other composed dungeons.
	 * @param 	maxXCoord
     * 		 	 The maximum size this dungeon has in the x-direction
     * @param 	maxYCoord
     * 		  	The maximum size this dungeon has in the y-direction
     * @param 	maxZCoord
     * 	  	  	The maximum size this dungeon has in the z-direction
     * @effect 	This new level is initialized as a dungeon with given coordinates as the maximum coordinates.
	 *          | super(maxXCoord, maxYCoord, maxZCoord)
     * @post	The fixed position is set equal to the origin.
	 * 			| new.getFixedPosition() == new Position(0, 0, 0)
	 */
	public Composite(long maxXCoord, long maxYCoord, long maxZCoord){
		super (maxXCoord, maxYCoord, maxZCoord);
		setFixedPosition(new Position(0, 0, 0));
	}
	
	/**
	 * Returns a hashmap containing the dungeons and their respective origins.
	 */
	@Basic
	public Map<Dungeon<S>, Position> getSubDungeons(){
		return dungeons;
	}
	
	/**
	 * A map containing the composed dungeons and their relative coordinates.
	 */
	private Map<Dungeon<S>, Position> dungeons = new HashMap<Dungeon<S>, Position>();
	
	
	/**
	 * Adds a subdungeon to this composed dungeon at the given position relative to the origin
	 * of this dungeon.
	 * @param 	subDungeon
	 * 		  	The subdungeon to be added
	 * @param 	position
	 * 		  	The position which indicates the relative distance between the origins
	 * 		  	of the subdungeon and this dungeon.
	 * @post 	The subdungeon is added at the specified position. 
	 * 		 	| getSubDungeons().containsKey(subDungeon) == true)
	 * 		 	| 		&& (getSubDungeons().containsValue (position) == true)
	 * @post	The containing dungeon of the subdungeon is set to this.
	 * 			| subDungeon.getContainingDungeon() == this
	 * @post	The fixed position (relative position) of the sub subdungeon is set to the given position.
	 * 			| subDungeon.getFixedPosition() == position
	 * @post	The subDungeon is placed at the specified position
	 * 			| getSubDungeons().get(subDungeon).equals(position)
	 * @effect 	The squares of the composite are set.
	 * 			| setCompositeSquares(subDungeon)
	 * @throws 	IllegalArgumentException
	 * 		   	The position is non-effective or the subDungeon is invalid.
	 * 		   	| (position == null) || !isValidSubDungeon(subDungeon)
	 * @throws 	IllegalDungeonPlacementException
	 * 		   	This dungeon can't have the given subdungeon as a subdungeon at the given position.
	 * 		   	| (!canHaveAsSubDungeon(subDungeon, position)) 
	 */
	public void addSubDungeon(Dungeon<S> subDungeon, Position position) throws IllegalArgumentException,IllegalDungeonPlacementException {
		if (!canHaveAsSubDungeon(subDungeon, position)) {
			throw new IllegalDungeonPlacementException();
		}
		dungeons.put(subDungeon, position);
		subDungeon.setContainingDungeon(this);
		subDungeon.setFixedPosition (position);
		setCompositeSquares(subDungeon);
		addNewPositions(subDungeon, position);
	}
	
	/**
	 * Sets the squares of the composite dungeon.
	 * @param 	subDungeon
	 * 		  	The subdungeon of which the squares have to be added.
	 * @effect 	|let
	 * 		   	|	newPos = new Position (pos.getXCoord() + subDungeon.getFixedPosition().getXCoord(), pos.getYCoord() + subDungeon.getFixedPosition().getYCoord(), pos.getZCoord()+ subDungeon.getFixedPosition().getZCoord())
	 * 		   	|	squareTemp = subDungeon.getSquareFromPosition(pos)
	 * 		   	|in	
	 * 		   	|	for each pos in subDungeon.getSquares().values():
	 * 		   	|		if (!getSquares().containsKey(squareTemp))
	 * 		   	|			then getSquares().put (squareTemp, newPos)
	 * 		   	|			updateBorders (newPos)
	 * @throws 	IllegalArgumentException
     *          The subdungeon has is non-effective or terminated.
     *          | !isValidSubDungeon(subDungeon)
	 */
	public void setCompositeSquares(Dungeon<S> subDungeon) throws IllegalArgumentException{
		if(!isValidSubDungeon(subDungeon))
			throw new IllegalArgumentException();
		for (Position pos : subDungeon.getSquares().values()){
			Position newPos = new Position (pos.getXCoord() + subDungeon.getFixedPosition().getXCoord(), pos.getYCoord() + subDungeon.getFixedPosition().getYCoord(), pos.getZCoord()+ subDungeon.getFixedPosition().getZCoord());
			S squareTemp = subDungeon.getSquareFromPosition(pos);
			if (!getSquares().containsKey(squareTemp)){
				squares.put(squareTemp, newPos);
				updateBorders (newPos);
			}
		}
	}
	
	
	/**
	 * List registering all possible positions of all other dungeons in this composite.
	 */
	ArrayList<Position> positions = new ArrayList<Position>();

	/**
	 * Adds all the possible positions of the given subdungeon to this composite relative to the given position.
	 * @param 	subDungeon
	 * 			The given subdungeon of which the positions have to be added.
	 * @param 	position
	 * 			The given relative position of the subdungeon
	 * @post	| for each pos in subDungeon.getAllPossiblePositions():
	 * 			| 	result.contains (new Position (pos.getXCoord()+position.getXCoord(), pos.getYCoord()+position.getYCoord(), pos.getZCoord()+position.getZCoord()))
	 */
	private void addNewPositions(Dungeon<S> subDungeon, Position position) {
		if(isValidSubDungeon(subDungeon) && (position != null)){
			for (Position pos : subDungeon.getAllPossiblePositions()){
				positions.add(new Position (pos.getXCoord()+position.getXCoord(), pos.getYCoord()+position.getYCoord(), pos.getZCoord()+position.getZCoord()));
			}
		}
	}

	/**
	 * Removes a subdungeon from this composite.
	 * @param 	subDungeon
	 * 		  	The subdungeon to be removed from this dungeon.
	 * @post 	The subdungeon is removed if it exists.
	 * 			| if (this.containsSubDungeon(subDungeon))
	 *		   	|	then getSubDungeons().remove(subDungeon)
	 */
	public void removeSubDungeon(Dungeon<S> subDungeon){
		if (this.containsSubDungeon(subDungeon))
			getSubDungeons().remove(subDungeon);
	}
	
	/**
	 *  Returns whether or not the given subdungeon is inside this composite.
     * 
     * @param 	subDungeon
     *          The subDungeon that will be checked
     * @return 	Whether or not this dungeon contains the square
     * 			| result == getSubDungeons().containsKey(subDungeon)
	 */
	public boolean containsSubDungeon(Dungeon<S> subDungeon){
		  return getSubDungeons().containsKey(subDungeon);
	}
	
	
	/** Returns whether or not the given subdungeon is a valid subdungeon.
	 * @param 	subDungeon
	 * 			The subdungeon to be checked
	 * @return 	The subdungeon must be effective and not terminated
	 * 			| result == (subDungeon != null) && !subDungeon.isTerminated()
	 */
	boolean isValidSubDungeon(Dungeon<S> subDungeon){
		return (subDungeon != null) && !subDungeon.isTerminated();
	}
	
	
	/**
	 * Returns whether or not this composite can contain the given subdungeon at the given position.
	 * 
	 * @param 	subDungeon
	 * 		  	The subdungeon to be checked.
	 * @param   position
	 * 		    The position which indicates the relative distance between the origins of
	 * 			 this composite and the to be added subdungeon.
	 * @return  |if (!subDungeonFitsIn(subDungeon, position))
	 *			| then result == false
	 * @return  |if (overlap(subDungeon, position))
	 *			| then result == false
	 * @throws 	IllegalArgumentException
     *          The subdungeon has is non-effective or terminated.
     *          | (position == null) || !isValidSubDungeon(subDungeon)
	 */
	private boolean canHaveAsSubDungeon (Dungeon<S> subDungeon, Position position) throws IllegalArgumentException{
		if(position == null)
            throw new IllegalArgumentException("Ineffective position");
		if (!isValidSubDungeon(subDungeon))
			throw new IllegalArgumentException("Invalid subDungeon");
		if (!subDungeonFitsIn(subDungeon, position) || hasOverlap(subDungeon, position))
			return false;
		return true;
	}
	
	/**
	 * Returns whether or not a given subdungeon fits in this composite with its origin on the given position.
	 * @param 	subDungeon
	 * 		  	The subdungeon to be checked.
	 * @param 	position
	 * 		  	The position of the origin the to be added subdungeon would get in this dungeon.
	 * @return 	False if the the maximum position of the to be added subdungeon exceeds the maximum positions of this composite.
	 * 			| if (((position.getXCoord() + subDungeon.getMaxXCoord()) > this.getMaxXCoord())
	 *		 	| 		|| (position.getYCoord() + subDungeon.getMaxYCoord() > this.getMaxYCoord())
	 *		 	| 			|| ((position.getZCoord() + subDungeon.getMaxZCoord()) > this.getMaxZCoord()))
 	 *		 	|	then result == false	
	 * 
	 * @throws 	IllegalArgumentException
     *          The subdungeon has is non-effective or terminated.
     *          |(subDungeon == null) 
     *          |		|| (subDungeon.isTerminated()) 
     *          |				|| (position == null)
	 */
	public boolean subDungeonFitsIn(Dungeon<S> subDungeon, Position position) throws IllegalArgumentException{
		if(!isValidSubDungeon(subDungeon) || position == null)
            throw new IllegalArgumentException("Subdungeon or position not initialized");
		if (position.getXCoord() + subDungeon.getMaxXCoord() > this.getMaxXCoord()
			 || position.getYCoord() + subDungeon.getMaxYCoord() > this.getMaxYCoord()
			 || position.getZCoord() + subDungeon.getMaxZCoord() > this.getMaxZCoord())	
			return false;
		return true;
	}
	
	/**
	 * Returns whether or not the given subdungeon with it its origin on the given position overlaps with
	 * a subdungeon in this composite dungeon.
	 * @param 	subDungeon
	 * 		  	The subdungeon of which has to be tested it overlaps with a subdungeon in this composite.
	 * @param 	position
	 * 		  	The relative position between the origin of this composite and the origin of the subdungeon.
	 * @return 	True if a position of the subdungeon overlaps with a possible position in the composite.
	 * 		   	| for each pos in subDungeon.getAllPossiblePositions():
	 * 		   	|	let 
	 * 		   	|		pos1 = new Position (pos.getXCoord()+position.getXCoord(), pos.getYCoord()+position.getYCoord(), pos.getZCoord()+position.getZCoord())
	 * 		   	|	in
	 * 		   	|		for each pos2 in positions
	 * 		   	|			if (pos1.equals(pos2))
	 * 		   	|				then result == true
	 */
	public boolean hasOverlap(Dungeon<S> subDungeon, Position position){
		for (Position pos: subDungeon.getAllPossiblePositions()){
			Position pos1 = new Position (pos.getXCoord()+position.getXCoord(), pos.getYCoord()+position.getYCoord(), pos.getZCoord()+position.getZCoord());
			for (Position pos2: positions){
				if (pos1.equals(pos2))
					return true;
			}
		}
		return false;
	}

	
	/**
	 * Returns the dungeon which contains this dungeon
	 */
	@Override
	@Basic
	public Composite<S> getContainingDungeon() {
		return containingDungeon;
	}

	/**
	 * Returns the rootdungeon of this dungeon.
	 */
	@Override
	public Dungeon<S> getRootDungeon() {
		if (getContainingDungeon() == null)
			return this;
		return getContainingDungeon().getRootDungeon();
	}	
	
	/**
	 * Returns all levels in this dungeon or in one of its subdungeons.
	 * @return 	|for each subDungeon in getSubDungeons().keySet()
	 * 		   	| if (subDungeon instanceof Level)
	 * 		   	|	then result.contains (subdungeon)
	 * 		   	| if (subDungeon instanceof Composite)
	 * 		   	|	then result.contains (subdungeon.getLevels())
	 */
	public List<Level<S>> getLevels(){
		List<Level<S>> levels = new ArrayList<Level<S>>();
		for (Dungeon<S> subDungeon: this.getSubDungeons().keySet()){
			if (subDungeon instanceof Level<?>){
				levels.add((Level<S>) subDungeon);
			}
			if (subDungeon instanceof Composite<?>){
				levels.addAll(((Composite<S>) subDungeon).getLevels());
			}
		}	
		return levels;
	}
	

	/**
	 * Returns all shafts in this dungeon or in one of its subdungeons.
	 * @return 	|for each dungeon in getSubDungeons().keySet()
	 * 		   	| if (subdungeon instanceof Shaft)
	 * 		   	|	then result.contains (subdungeon)
	 * 		   	| if (subdungeon instanceof Composite)
	 * 		   	|	then result.contains (subdungeon.getShafts())
	 */
	public List<Shaft<S>> getShafts(){
		List<Shaft<S>> shafts = new ArrayList<Shaft<S>>();
		for (Dungeon<S> subdungeon: this.getSubDungeons().keySet()){
			if (subdungeon instanceof Shaft<?>){
				shafts.add((Shaft<S>) subdungeon);
			}
			if (subdungeon instanceof Composite<?>){
				shafts.addAll(((Composite<S>) subdungeon).getShafts());
			}
		}	
		return shafts;
	}

	
	/**
	 * Return an iterator returning all the squares in this 
	 * composed dungeon.
	 */
	@Override
	public Iterator<S> iterator() {
		return new Iterator<S>() {
			public Iterator<S> currentIterator;
			Iterator<Dungeon<S>> mainIterator = getSubDungeons().keySet().iterator();

			{
				if (mainIterator.hasNext()) {
					currentIterator = ((Dungeon<S>) mainIterator.next())
							.iterator();
				}
			}

			public boolean hasNext() {
				return (currentIterator.hasNext() || mainIterator.hasNext());
			}

			public S next() {
				if (!hasNext())
					throw new NoSuchElementException();

				if (currentIterator.hasNext())
					return (S) currentIterator.next();

				else {
					
					while (!currentIterator.hasNext() && mainIterator.hasNext()){
						currentIterator = ((Dungeon<S>) mainIterator.next())
						.iterator();
					}
						return (S) currentIterator.next();
				}
			}

			public void remove() {
				throw new UnsupportedOperationException();
			}

		};
	}
	
	/**
	 * Sets the the containing dungeon to the given value.
	 */
	@Override
	public void setContainingDungeon(Composite<S> dungeon) {
		containingDungeon = dungeon;	
	}
	
	/**
	 * Variable registering the containing dungeon. Standard value is null.
	 */
	private Composite<S> containingDungeon;

	
	/**
	 * A method returning all the possible positions that can exist in this composite.
	 * 
	 * @return	An arraylist of positions. 
	 * 			| let 
	 * 			|	i = 0, j = 0, k = 0
	 * 			| in
	 * 			| 	for i <= getMaxXCoord(),i++:
	 * 			|		for j <= getMaxYCoord(),j++:
	 * 			|			for k <= getMaxZCoord(),k++:
	 * 			|				result.contains (new Position(i, j, k))
	 */
	@Override
	public ArrayList<Position> getAllPossiblePositions() {
		ArrayList<Position> positions = new ArrayList<Position>();

		for (long i = 0; i<= getMaxXCoord();i++)
		{
			for (long j = 0; j<= getMaxYCoord();j++)
			{
				for (long k = 0; k<= getMaxZCoord();k++)
				{
					positions.add(new Position(i, j, k));
				}
			}
		}
		
		return positions;
	}

	
}
